var Signature = (function(){
    function Signature(selector,options){
        this.options=options || {};
        this.pressure= false;
        this.lines = [];
        this.step = -1;
        this.canvas = document.querySelector(selector);
        if(!this.canvas){
            throw new Error(selector +'element 不存在')
            return
        }
        this.canvasWidth = this.canvas.width;
        this.canvasHeight = this.canvas.height;
        this.isSupportTouch = 'ontouchstart' in window;
        this.events = 'ontouchstart' in window ? ['touchstart', 'touchmove', 'touchend'] : ['mousedown', 'mousemove', 'mouseup','mouseleave'];
        this.ctx = this.canvas.getContext('2d');
        this.ctx.lineCap='round';
        this.ctx.lineJoin='round';
        this.changeFun =null;
        this.lines=[];
        this.useBezier=false
        this.bezierPoints=[];
        this.lastVelocity=0;
        this.options.speed = this.options.speed ||0.7
        this.init(this);
    }
    Signature.prototype={
        constructor:Signature,
        init:function(that){
            that.canvas.width=this.canvasWidth;
            that.canvas.height=this.canvasHeight;
            that.canvas.addEventListener(that.events[0], function() {that.startEventHandler(window.event)},false);
            that.canvas.addEventListener(that.events[1],function() { that.moveEventHandler(window.event)}, false);
            that.canvas.addEventListener(that.events[2], function() {that.endEventHandler(window.event)}, false);
            that.events[3] &&   that.canvas.addEventListener(that.events[3], function() {that.endEventHandler(window.event)}, false);
        },
        clear: function() {
			this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
			this.ctx.closePath();
		},
        reset(){
            this.lines=[];
            this.step=-1;
            this.clear();
            this.lastVelocity=0;
        },
        toBase64:function(type){
            type= type?type:'png';
            switch (type) {
				case 'png':
					this.dataurl = this.canvas.toDataURL('image/png');
					break;
				case 'jpg':
					this.dataurl = this.canvas.toDataURL('image/jpeg', 0.8);
					break;
			}
			return this.dataurl
        },
        setStyle: function(obj){
			Object.assign(this.options,obj)
		},
        changePressure:function(boolen){
            this.pressure=boolen
        },
        changeBezier:function(boolen){
            this.useBezier=boolen
        },
        startEventHandler: function(event) {
			event.preventDefault();
            if(this.animating) return;
            this.drawing=true;
			this.evt = this.isSupportTouch ? event.touches[0] : event;
			this.coverPos = this.canvas.getBoundingClientRect();
			this.mouseX = this.evt.clientX - this.coverPos.left;
			this.mouseY = this.evt.clientY - this.coverPos.top;
            this.startDrawLine(new this.Point(this.mouseX,this.mouseY))
		},
		moveEventHandler: function(event) {
			event.preventDefault();
            if(!this.drawing) return;
			this.evt = this.isSupportTouch ? event.touches[0] : event;
			this.coverPos = this.canvas.getBoundingClientRect();
			this.mouseX = this.evt.clientX - this.coverPos.left;
			this.mouseY = this.evt.clientY - this.coverPos.top;
            this.drawLine(new this.Point(this.mouseX,this.mouseY),this.lines)
		},
		endEventHandler: function(event) {
            this.drawing=false;
			event.preventDefault();
			window.removeEventListener(this.events[1], this.moveEventHandler, false);
			window.removeEventListener(this.events[2], this.endEventHandler, false);

            if(this.bezierPoints) this.bezierPoints=[];
		},
        revoke:function(){
            if(this.step<0) return;
            this.clear();
            this.step =-1;
            this.lines.pop();
            var revokeLines=JSON.parse(JSON.stringify(this.lines)) ;
            this.lines=[];
            // if(this.step>-1){
            this.lastVelocity=0;
            this.lastWidth=null
            for(let i =0;i<revokeLines.length;i++){
                if(this.bezierPoints) this.bezierPoints=[];
                var setting ={
                    color:revokeLines[i].color,
                    lineWidth:revokeLines[i].lineWidth,
                    pressure:revokeLines[i].pressure,
                    useBezier:revokeLines[i].useBezier,
                }
                this.startDrawLine(new this.Point(revokeLines[i].start.x,revokeLines[i].start.y,revokeLines[i].start.time) ,setting)
                for(let j=1;j<revokeLines[i].points.length;j++){
                    var lastPoint = revokeLines[i].points[j];
                    this.drawLine(new this.Point(lastPoint.x,lastPoint.y,lastPoint.time),this.lines)
                }
            }
            // }
        },
        reDrawLineAnimate:function(step){
            this.initTime?this.initTime:this.lines[step].points[0].time;
            return new Promise((resolve,reject)=>{
                if(!this.lines[step]){
                    resolve()
                }else{
                    var pointsIdx = 0;
                    var lastPoint,nextPoint;
                    var that =this;
                    var lines = that.lines
                    var lastTime =  that.lines[step].points[that.lines[step].points.length-1]
                    if(that.bezierPoints) that.bezierPoints=[];
                    that.lastVelocity=0;
                    that.lastWidth=null
                    that.reDrawTimer = setInterval(function(){
                        lastPoint = that.lines[step].points[pointsIdx];
                        nextPoint = that.lines[step].points[pointsIdx+1]
                        if(!nextPoint || that.initTime>=lastTime.time){
                            clearInterval(that.reDrawTimer);
                            resolve()
                        }else{
                            if(lastPoint.time===that.initTime){
                                that.ctx.beginPath();
                                that.ctx.lineWidth= lines[step].lineWidth;
                                that.ctx.strokeStyle= lines[step].color;
                                const presure=lines[step].pressure;
                                const useBezier=lines[step].useBezier;
                                if (useBezier) {
                                    var points = that.createBezierPoint(nextPoint)
                                    if(points&&points.control2){
                                        if(presure){
                                            const widthDelta = (points.endWidth - points.startWidth || 0);
                                            that.ctx.fillStyle= lines[step].color;
                                            const drawSteps = Math.floor(points.length()) * 2 || 0;
                                            for (let i = 0; i < drawSteps; i += 1) {
                                                const t = i / drawSteps;
                                                const tt = t * t;
                                                const ttt = tt * t;
                                                const u = 1 - t;
                                                const uu = u * u;
                                                const uuu = uu * u;
                                                let x1 = uuu * points.startPoint.x;
                                                x1 += 3 * uu * t * points.control1.x;
                                                x1 += 3 * u * tt * points.control2.x;
                                                x1 += ttt * points.endPoint.x;
                                                let y1 = uuu * points.startPoint.y;
                                                y1 += 3 * uu * t * points.control1.y;
                                                y1 += 3 * u * tt * points.control2.y;
                                                y1 += ttt * points.endPoint.y;
                                                const width = points.startWidth + ttt * widthDelta;
                                                that.ctx.arc(x1, y1, width, 0, 2 * Math.PI, false);
                                            }
                                            that.ctx.closePath()
                                            that.ctx.fill();
                                        }else{
                                            that.ctx.moveTo(points.startPoint.x, points.startPoint.y);
                                            that.ctx.quadraticCurveTo(points.control1.x,points.control1.y,point.x,point.y);   
                                            that.ctx.stroke()
                                            that.ctx.closePath()
                                        }
                                    }
                                }else{
                                    that.ctx.moveTo(lastPoint.x, lastPoint.y);
                                    if(presure){
                                        if(lines[step].points.length>0){
                                             const widths = that.calcCurveWidths(lastPoint,nextPoint) ;
                                             const widthDelta = widths.end - widths.start;
                                             that.ctx.fillStyle=lines[step].color;
                                             const drawSteps = Math.floor(nextPoint.distanceTo(lastPoint)) * 2 || 0;
                                             for (let i = 1; i <= drawSteps; i += 1) {
                                                const t =i/drawSteps;
                                                const x1=(nextPoint.x-lastPoint.x)*t +lastPoint.x;
                                                const y1=(nextPoint.y-lastPoint.y)*t +lastPoint.y;
                                                const width = widths.start + t * widthDelta;
                                                that.ctx.arc(x1, y1, width || 0.5, 0, 2 * Math.PI, false);
                                            }
                                            that.ctx.closePath()
                                            that.ctx.fill();
                                        }
                                    }else{
                                        that.ctx.lineTo(nextPoint.x, nextPoint.y);
                                        that.ctx.stroke()
                                        that.ctx.closePath()
                                    }
                                }

                                pointsIdx++
                            }
                        }
                        that.initTime++
                    },1)
                }
            })
        },
        reDrawAnimateAllasync:async function(){
            this.clear()
            this.animating = true;
            this.initTime =this.lines[0]?this.lines[0].points[0].time:0;
            for(var i =0;i<this.lines.length;i++){
                await this.reDrawLineAnimate(i)
            }    
           this.animating = false;
        },
        startDrawLine:function(points,setting){
            setting = setting || {
                color:this.options.color || '#000000',
                lineWidth:this.options.lineWidth || 2,
                pressure:this.pressure,
                useBezier:this.useBezier,
            }
            this.step++
            this.lines[this.step]={
                start:points,
                color:setting.color ,
                lineWidth:setting.lineWidth ,
                pressure:setting.pressure ,
                useBezier:setting.useBezier,
                points:[]
            }
            this.lines[this.step].points.push(points)
        },
        animation: function(arg){
            var speed = arg.speed||1;
            var skip = arg.skip || false;
            var allTime =arg.time ;
            var duringTime = this.lines[this.lines.length-1].points[this.lines[this.lines.length-1].points.length-1].time-this.lines[0].start.time
            if(allTime<duringTime && !skip){
                speed = duringTime/allTime
            }
            return new Promise((resolve,reject)=>{
                
                var that = this;
                this.animating = true;
                this.clear();
                this.step =-1;
                var animateLines=JSON.parse(JSON.stringify(this.lines)) ;
                this.lines=[];
                // if(this.step>-1){
                this.lastVelocity=0;
                this.lastWidth=null
                foreachAnimate(0,function(){
                    resolve()
                    that.animating = false;
                })
                function foreachAnimate(idx,callback){
                    if (idx<animateLines.length) {
                        if(that.bezierPoints) that.bezierPoints=[];
                        var setting ={
                            color:animateLines[idx].color,
                            lineWidth:animateLines[idx].lineWidth,
                            pressure:animateLines[idx].pressure,
                            useBezier:animateLines[idx].useBezier,
                        }
                        that.startDrawLine(new that.Point(animateLines[idx].start.x,animateLines[idx].start.y,animateLines[idx].start.time) ,setting)
                        forDrawLine(idx,0,function(time){
                            if(skip){
                                idx++
                                foreachAnimate(idx,callback)
                            }else{
                                idx++
                                var waitTime =0
                                if(animateLines[idx]) waitTime = animateLines[idx].start.time-time;
                                setTimeout(function(){
                                    foreachAnimate(idx,callback)
                                },waitTime) 
                            }
                        })
                    }else{
                       
                        callback&&callback()
                    }
                }
                function forDrawLine(step,idx,callback){
                    if(idx<animateLines[step].points.length){
                        var lastPoint = animateLines[step].points[idx];
                        that.drawLine(new that.Point(lastPoint.x,lastPoint.y,lastPoint.time),that.lines)
                        var time=0
                        if(idx<animateLines[step].points.length-1){
                            time= animateLines[step].points[idx+1].time-lastPoint.time
                        }
                        setTimeout(function(){
                            idx++
                            forDrawLine(step,idx,callback)
                        },parseInt(time/speed))
                    }else{
                        var time = animateLines[step].points[animateLines[step].points.length-1].time
                        callback&&callback(parseInt(time/speed))
                    }
                }
            })
           

        },
        drawLine:function(point,lines,step){
            step = step || this.step
            this.ctx.beginPath();
            this.ctx.lineWidth= lines[step].lineWidth;
            this.ctx.strokeStyle= lines[step].color;
            const presure=lines[step].pressure;
            const useBezier=lines[step].useBezier;
            var lastPoint = lines[step].points[lines[step].points.length-1];
            if (useBezier) {
                var points = this.createBezierPoint(point)
                if(points&&points.control2){
                    if(presure){
                        const widthDelta = (points.endWidth - points.startWidth || 0);
                        this.ctx.fillStyle= lines[step].color;
                        const drawSteps = Math.floor(points.length()) * 2 || 0;
                        for (let i = 0; i < drawSteps; i += 1) {
                            const t = i / drawSteps;
                            const tt = t * t;
                            const ttt = tt * t;
                            const u = 1 - t;
                            const uu = u * u;
                            const uuu = uu * u;
                            let x1 = uuu * points.startPoint.x;
                            x1 += 3 * uu * t * points.control1.x;
                            x1 += 3 * u * tt * points.control2.x;
                            x1 += ttt * points.endPoint.x;
                            let y1 = uuu * points.startPoint.y;
                            y1 += 3 * uu * t * points.control1.y;
                            y1 += 3 * u * tt * points.control2.y;
                            y1 += ttt * points.endPoint.y;
                            const width = points.startWidth + ttt * widthDelta;
                            this.ctx.arc(x1, y1, width, 0, 2 * Math.PI, false);
                        }
                        this.ctx.closePath()
                        this.ctx.fill();
                    }else{
                        this.ctx.moveTo(points.startPoint.x, points.startPoint.y);
                        this.ctx.quadraticCurveTo(points.control1.x,points.control1.y,point.x,point.y);   
                        this.ctx.stroke()
                        this.ctx.closePath()
                    }
                }
            }else{
                this.ctx.moveTo(lastPoint.x, lastPoint.y);
                if(presure){
                    if(lines[step].points.length>0){
                        const lastPoints = lines[step].points[lines[step].points.length-1]
                         const widths = this.calcCurveWidths(lastPoints,point) ;
                         const widthDelta = widths.end - widths.start;
                         this.ctx.fillStyle=lines[step].color;
                         const drawSteps = Math.floor(point.distanceTo(lastPoints)) * 2 || 0;
                         for (let i = 1; i <= drawSteps; i += 1) {
                            const t =i/drawSteps;
                            const x1=(point.x-lastPoints.x)*t +lastPoints.x;
                            const y1=(point.y-lastPoints.y)*t +lastPoints.y;
                            const width = widths.start + t * widthDelta;
                            this.ctx.arc(x1, y1, width || 0.5, 0, 2 * Math.PI, false);
                        }
                        this.ctx.closePath()
                        this.ctx.fill();
                    }
                }else{
                    this.ctx.lineTo(point.x, point.y);
                    this.ctx.stroke()
                    this.ctx.closePath()
                }
            }
            lines[this.step].points.push(point)   
        },
        calcCurveWidths:function(startPoint, endPoint){
            const velocity = this.options.speed  * (endPoint.velocityFrom(startPoint) || 0)  + (1 - this.options.speed) * this.lastVelocity;
			const newWidth = Math.max((this.options.maxWidth || 4) / (velocity + 1), (this.options.minWidth) || 1);
			const widths = {
				end: newWidth,
				start: this.lastWidth
			};
			this.lastVelocity = velocity;
			this.lastWidth = newWidth;
			return widths;
        },
        createBezierPoint(point){
            this.bezierPoints.push(point);
            if(this.bezierPoints.length>2){
                if (this.bezierPoints.length === 3) {
                    this.bezierPoints.unshift(this.bezierPoints[0]);
                }
                const widths = this.calcCurveWidths(this.bezierPoints[1], this.bezierPoints[2]);
                let curve  = new this.Bezier;
                curve =curve.fromPoints(this.bezierPoints, widths,this);
                this.bezierPoints.shift();
                return curve;

            }
            return null;
        },
        Bezier:(function(){
            function Bezier(startPoint, control2, control1, endPoint, startWidth, endWidth){
                this.startPoint = startPoint;
                this.control2 = control2;
                this.control1 = control1;
                this.endPoint = endPoint;
                this.startWidth = startWidth;
                this.endWidth = endWidth;
                
            }
            Bezier.prototype={
                constructor:Bezier,
                fromPoints:function(points, widths,that) {
                    const c2 = this.calculateControlPoints(that,points[0], points[1], points[2]).c2;
                    const c3 = this.calculateControlPoints(that,points[1], points[2], points[3]).c1;
                    return new Bezier(points[1], c2, c3, points[2], widths.start, widths.end);
                },
                calculateControlPoints:function(that,s1, s2, s3) {
                    const dx1 = s1.x - s2.x;
                    const dy1 = s1.y - s2.y;
                    const dx2 = s2.x - s3.x;
                    const dy2 = s2.y - s3.y;
                    const m1 = { x: (s1.x + s2.x) / 2.0, y: (s1.y + s2.y) / 2.0 };
                    const m2 = { x: (s2.x + s3.x) / 2.0, y: (s2.y + s3.y) / 2.0 };
                    const l1 = Math.sqrt((dx1 * dx1) + (dy1 * dy1));
                    const l2 = Math.sqrt((dx2 * dx2) + (dy2 * dy2));
                    const dxm = (m1.x - m2.x);
                    const dym = (m1.y - m2.y);
                    const k = l2 / (l1 + l2);
                    const cm = { x: m2.x + (dxm * k), y: m2.y + (dym * k) };
                    const tx = s2.x - cm.x;
                    const ty = s2.y - cm.y;
                    return {
                      c1: new that.Point(m1.x + tx, m1.y + ty),
                      c2: new that.Point(m2.x + tx, m2.y + ty),
                    };
                },
                length:function() {
                const steps = 10;
                let length = 0;
                let px;
                let py;
                for (let i = 0; i <= steps; i += 1) {
                    const t = i / steps;
                    const cx = this.points(t, this.startPoint.x, this.control1.x, this.control2.x, this.endPoint.x);
                    const cy = this.points(t, this.startPoint.y, this.control1.y, this.control2.y, this.endPoint.y);
                    if (i > 0) {
                    const xdiff = cx - px;
                    const ydiff = cy - py;
                    length += Math.sqrt((xdiff * xdiff) + (ydiff * ydiff));
                    }
                    px = cx;
                    py = cy;
                }
                return length;
                },
                points:function(t, start, c1, c2, end) {
                    return (start * (1.0 - t) * (1.0 - t) * (1.0 - t))
                        + (3.0 * c1 * (1.0 - t) * (1.0 - t) * t)
                        + (3.0 * c2 * (1.0 - t) * t * t)
                        + (end * t * t * t);
                },
                
            }
            return Bezier;
        }()),
        Point:(function(){
            function Point(x,y,time){
                this.x = x;
                this.y = y;
                this.time = time || Date.now();
            }
            Point.prototype={
                constructor:Point,
                distanceTo:function(start) {
                    return Math.sqrt(Math.pow(this.x - start.x, 2) + Math.pow(this.y - start.y, 2));
                },
                equals:function(other) {
                    return this.x === other.x && this.y === other.y && this.time === other.time;
                },
                velocityFrom:function(start) {
                    return (this.time !== start.time) ? this.distanceTo(start) / (this.time - start.time) : 0;
                },
            }
            return Point
        }())
    }
    return Signature;
}())